
在這篇文章中,我們將探討一段精妙的 TypeScript 程式碼,它展示了如何透過 泛型 來創建一個動態且型別安全的服務系統。
這個範例可以用來在應用中定義並處理一系列的服務方法,透過 TypeScript 的型別系統保證每個服務方法的參數和行為都符合預期。
TypeScript: TS Playground - An online editor for exploring TypeScript and JavaScript
ServiceDefinition)export type ServiceDefinition = {
[x: string]: MethodDefinition;
};
這段程式碼定義了一個服務的基本結構,ServiceDefinition 是一個物件,物件中的每個鍵都是一個服務方法的名稱,對應的值是該方法的定義,這些定義會告訴我們這個方法接收什麼樣的參數。
MethodDefinition)export type MethodDefinition = {
[x: string]: StringConstructor | NumberConstructor;
};
MethodDefinition 定義了每個方法可以接受的參數型別。這裡,我們允許每個方法的參數是 String 或 Number,這代表服務方法的參數型別只會是字串或數字。
ServiceObject)export type ServiceObject<T extends ServiceDefinition> = {
[P in keyof T]: ServiceMethod<T[P]>
};
這個型別用來定義我們的服務物件,ServiceObject 會根據 ServiceDefinition 來生成對應的方法。這樣每個定義的服務方法都會被轉換成 ServiceMethod,並且具備動態的參數型別。
ServiceMethod)export type ServiceMethod<T extends MethodDefinition> = {} extends T
? () => boolean
: (payload: RequestPayload<T>) => boolean;
ServiceMethod 是核心邏輯,它依據方法的定義來決定這個方法是否接收參數:
T) 是空的,則這個方法不需要參數,會是一個單純的 () => boolean 方法。payload,並返回 boolean。payload (RequestPayload)export type RequestPayload<T extends MethodDefinition> = {} extends T
? undefined
: { [P in keyof T]: TypeFromConstructor<T[P]> };
RequestPayload 會根據 MethodDefinition 自動推斷這個方法需要的 payload 型別。如果方法定義是空的,那 payload 會是 undefined;如果方法定義了參數,那麼 payload 會是對應的物件,物件中的每個屬性對應到方法中的參數。
TypeFromConstructor)export type TypeFromConstructor<T> = T extends StringConstructor
? string
: T extends NumberConstructor ? number : any;
這裡是型別轉換邏輯,根據 Constructor 類型決定對應的實際型別:
StringConstructor 轉換為 string。NumberConstructor 轉換為 number。這樣的設計可以保證我們在呼叫服務方法時,所傳遞的參數符合正確的型別。
RequestHandler 和 RequestObjectexport type RequestHandler<T extends ServiceDefinition> = (req: RequestObject<T>) => boolean;
export type RequestObject<T extends ServiceDefinition> = {
[P in keyof T]: {
message: P;
payload: RequestPayload<T[P]>;
}
}[keyof T];
RequestHandler 定義了處理請求的邏輯,會接收一個 RequestObject 並返回 boolean。RequestObject 則是根據服務定義動態生成的物件,包含 message(方法名稱)和 payload(參數)。createServicefunction createService<S extends ServiceDefinition>(
serviceDef: S,
handler: RequestHandler<S>,
): ServiceObject<S> {
const service: Record<string, Function> = {};
for (const name in serviceDef) {
service[name] = (payload: any) => handler({ message: name, payload });
}
return service as ServiceObject<S>;
}
這個函式是核心功能,用於創建服務物件。它接收一個服務定義 serviceDef 和一個處理請求的 handler 函式,然後根據服務定義動態生成對應的方法。
每個方法在被呼叫時,會將 message 和 payload 傳給 handler,這樣可以對每個請求進行統一處理。
const serviceDefinition = {
open: { filename: String },
insert: { pos: Number, text: String },
delete: { pos: Number, len: Number },
close: {},
};
這裡定義了四個服務方法:
open:需要一個 filename 參數(字串)。insert:需要 pos(數字)和 text(字串)兩個參數。delete:需要 pos 和 len 兩個數字參數。close:不需要參數。接下來使用 createService 來創建服務:
const service = createService(serviceDefinition, req => {
switch (req.message) {
case 'open':
break;
case 'insert':
req.payload; // 可取得對應的 payload
break;
default:
break;
}
return true;
});
這個 handler 函式會根據請求的 message 進行對應的操作處理。
service.close();
service.open({ filename: 'text.txt' });
最後,我們可以像使用一般物件方法一樣來呼叫服務方法。service.close() 不需要參數,而 service.open({ filename: 'text.txt' }) 則需要提供一個 filename 參數。
型別定義錯誤:當 MethodDefinition 定義的參數不符合 StringConstructor 或 NumberConstructor 時,可能導致型別錯誤。因此,務必確保在 ServiceDefinition 中定義的方法參數型別正確。
處理函式錯誤:如果 handler 沒有正確處理 RequestObject 中的 message 和 payload,可能會導致 undefined 或型別錯誤。
遞歸型別推斷問題:在某些情況下,遞歸型別推斷可能會遇到型別系統的限制,導致推斷失敗。這時可以考慮進一步簡化型別邏輯。
動態服務系統設計:透過 TypeScript 泛型與高階型別,實現靈活的服務方法定義,能動態處理多種不同的請求。
型別安全保障:使用 JSONified 和遞歸型別推斷,保證每個服務方法的參數和返回值符合預期,避免型別錯誤。
高度可擴展性:新增或修改服務方法時,只需更新 ServiceDefinition,型別系統會自動推斷並生成對應的邏輯。
undefined 處理得當:使用 UndefinedAsNull 將 undefined 轉換為 null,避免 JSON 格式不支援 undefined 的問題。
簡化程式碼邏輯:通過型別系統的靈活應用,減少手動定義類型的需求,提升程式碼可讀性與可維護性。
export type ServiceDefinition = {
[x: string]: MethodDefinition;
};
export type MethodDefinition = {
[x: string]: StringConstructor | NumberConstructor;
};
export type ServiceObject<T extends ServiceDefinition> = {
[P in keyof T]: ServiceMethod<T[P]>
};
export type ServiceMethod<T extends MethodDefinition> = {} extends T
? () => boolean
: (payload: RequestPayload<T>) => boolean;
export type RequestPayload<T extends MethodDefinition> = {} extends T
? undefined
: { [P in keyof T]: TypeFromConstructor<T[P]> };
export type TypeFromConstructor<T> = T extends StringConstructor
? string
: T extends NumberConstructor ? number : any;
export type RequestHandler<T extends ServiceDefinition> = (req: RequestObject<T>) => boolean;
export type RequestObject<T extends ServiceDefinition> = {
[P in keyof T]: {
message: P;
payload: RequestPayload<T[P]>;
}
}[keyof T];
function createService<S extends ServiceDefinition>(
serviceDef: S,
handler: RequestHandler<S>,
): ServiceObject<S> {
const service: Record<string, Function> = {};
for (const name in serviceDef) {
service[name] = (payload: any) => handler({ message: name, payload });
}
return service as ServiceObject<S>;
}
const serviceDefinition = {
open: { filename: String },
insert: { pos: Number, text: String },
delete: { pos: Number, len: Number },
close: {},
};
const service = createService(serviceDefinition, req => {
switch (req.message) {
case 'open':
break;
case 'insert':
req.payload
break;
default:
// req.
break;
}
return true;
});
service.close();
service.open({ filename: 'text.txt' });
嘿嘿~看到這裡你已經掌握了 TypeScript 的型別魔法!
別害怕嘗試新技巧,每一次的挑戰都是進步的機會~
快把這些知識用在你的專案裡,讓程式碼更靈活、更安全!
加油,寫程式就是這麼好玩!💻✨